/*
 * Copyright (c) 2020 libinal's testing project.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * imitations under the License.
 */

package com.platform.comm.repository;

import com.querydsl.core.NonUniqueResultException;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.Querydsl;
import org.springframework.data.jpa.repository.support.QuerydslJpaRepository;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

/**
 * Custom base repository implementation
 *
 * @author libin
 */
@SuppressWarnings({"SpringJavaConstructorAutowiringInspection", "deprecation"})
public class CustomBaseRepositoryImpl<T, ID extends Serializable> extends QuerydslJpaRepository<T, ID> implements CustomBaseRepository<T, ID> {

    private static final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE;

    private final EntityPath<T> path;
    private final PathBuilder<T> builder;
    private final Querydsl querydsl;
    private final EntityManager entityManager;

    public CustomBaseRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
        this(entityInformation, entityManager, DEFAULT_ENTITY_PATH_RESOLVER);
    }

    public CustomBaseRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager, EntityPathResolver resolver) {
        super(entityInformation, entityManager, resolver);
        this.path = resolver.createPath(entityInformation.getJavaType());
        this.builder = new PathBuilder<>(path.getType(), path.getMetadata());
        this.entityManager = entityManager;
        this.querydsl = new Querydsl(entityManager, builder);
    }

    @Override
    public EntityManager entityManager() {
        return this.entityManager;
    }

    @Override
    public Optional<T> findOne(Predicate predicate, Consumer<JPQLQuery<?>> consumer) {
        try {
            return Optional.ofNullable(createQuery(consumer, predicate).select(path).fetchOne());
        } catch (NonUniqueResultException ex) {
            throw new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex);
        }
    }

    @Override
    public Optional<Tuple> findOne(Predicate predicate, Expression<?>... expressions) {
        return findOne(predicate, null, expressions);
    }

    @Override
    public Optional<Tuple> findOne(Predicate predicate, Consumer<JPQLQuery<?>> consumer, Expression<?>... expressions) {

        try {
            return Optional.ofNullable(createQuery(consumer, predicate).select(expressions).fetchOne());
        } catch (NonUniqueResultException ex) {
            throw new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex);
        }
    }

    @Override
    public List<T> findAll(Predicate predicate, Consumer<JPQLQuery<?>> consumer) {
        return createQuery(consumer, predicate).select(path).fetch();
    }

    @Override
    public List<Tuple> findAll(Predicate predicate, Expression<?>... expressions) {
        return createQuery(predicate).select(expressions).fetch();
    }

    @Override
    public List<Tuple> findAll(Predicate predicate, Sort sort, Expression<?>... expressions) {
        Assert.notNull(sort, "Sort must not be null!");
        return this.executeSorted(this.createQuery(predicate).select(expressions), sort);
    }

    @Override
    public List<Tuple> findAll(Predicate predicate, Consumer<JPQLQuery<?>> consumer, Expression<?>... expressions) {
        return createQuery(consumer, predicate).select(expressions).fetch();
    }

    @Override
    public List<Tuple> findAll(Consumer<JPQLQuery<?>> consumer, Expression<?>... expressions) {
        return createQuery(consumer).select(expressions).fetch();
    }

    @Override
    public List<Tuple> findAll(Consumer<JPQLQuery<?>> consumer, Sort sort, Expression<?>... expressions) {

        Assert.notNull(sort, "Sort must not be null!");

        return executeSorted(createQuery(consumer).select(expressions), sort);
    }

    @Override
    public List<Tuple> findAll(Predicate predicate, Consumer<JPQLQuery<?>> consumer, Sort sort, Expression<?>... expressions) {

        Assert.notNull(sort, "Sort must not be null!");

        return executeSorted(createQuery(consumer, predicate).select(expressions), sort);
    }

    @Override
    public Page<T> findAll(Predicate predicate, Pageable pageable, Consumer<JPQLQuery<?>> consumer) {
        Assert.notNull(pageable, "Pageable must not be null!");
        final JPQLQuery<?> countQuery = createCountQuery(consumer, predicate);
        JPQLQuery<T> query = querydsl.applyPagination(pageable, createQuery(consumer, predicate).select(path));
        return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount);
    }

    @Override
    public Page<Tuple> findAll(Predicate predicate, Pageable pageable, Expression<?>... expressions) {
        return findAll(predicate, pageable, null, expressions);
    }

    @Override
    public Page<Tuple> findAll(Predicate predicate, Pageable pageable, Consumer<JPQLQuery<?>> consumer, Expression<?>... expressions) {
        Assert.notNull(pageable, "Pageable must not be null!");
        final JPQLQuery<?> countQuery = createCountQuery(consumer, predicate);
        int realCount = (int) countQuery.fetchCount();
        int totalPage = (realCount + pageable.getPageSize() - 1) / pageable.getPageSize();
        if (pageable.getPageNumber() > totalPage) {
            pageable = PageRequest.of(Math.max(totalPage - 1, 0), pageable.getPageSize(), pageable.getSort());
        }
        JPQLQuery<Tuple> query = querydsl.applyPagination(pageable, createQuery(consumer, predicate).select(expressions));
        return PageableExecutionUtils.getPage(query.fetch(), pageable, () -> realCount);
    }


    protected JPQLQuery<?> createQuery(@Nullable Consumer<JPQLQuery<?>> consumer, Predicate... predicate) {
        JPQLQuery<?> query = super.createQuery(predicate);
        if (consumer != null) {
            consumer.accept(query);
        }
        return query;
    }

    protected JPQLQuery<?> createCountQuery(@Nullable Consumer<JPQLQuery<?>> consumer, Predicate... predicate) {
        JPQLQuery<?> query = super.createCountQuery(predicate);
        if (consumer != null) {
            consumer.accept(query);
        }
        return query;
    }

    protected List<Tuple> executeSorted(JPQLQuery<Tuple> query, Sort sort) {
        return querydsl.applySorting(sort, query).fetch();
    }
}
